/* Copyright (c) 2010, Carl Burch. License information is located in the
 * com.cburch.logisim.Main source code and at www.cburch.com/logisim/. */

package com.cburch.logisim.proj;

import java.util.HashMap;
import java.util.LinkedList;

import javax.swing.JFileChooser;

import com.cburch.logisim.circuit.Circuit;
import com.cburch.logisim.circuit.CircuitListener;
import com.cburch.logisim.circuit.CircuitState;
import com.cburch.logisim.circuit.Simulator;
import com.cburch.logisim.circuit.SubcircuitFactory;
import com.cburch.logisim.file.Loader;
import com.cburch.logisim.file.LogisimFile;
import com.cburch.logisim.file.LibraryEvent;
import com.cburch.logisim.file.LibraryListener;
import com.cburch.logisim.file.Options;
import com.cburch.logisim.gui.log.LogFrame;
import com.cburch.logisim.gui.main.Canvas;
import com.cburch.logisim.gui.main.Frame;
import com.cburch.logisim.gui.main.Selection;
import com.cburch.logisim.gui.main.SelectionActions;
import com.cburch.logisim.gui.opts.OptionsFrame;
import com.cburch.logisim.tools.AddTool;
import com.cburch.logisim.tools.Library;
import com.cburch.logisim.tools.Tool;
import com.cburch.logisim.util.EventSourceWeakSupport;
import com.cburch.logisim.util.JFileChoosers;

public class Project {
    private static final int MAX_UNDO_SIZE = 64;

    private static class ActionData {
        CircuitState circuitState;
        Action action;

        public ActionData(CircuitState circuitState, Action action) {
            this.circuitState = circuitState;
            this.action = action;
        }
    }

    private class MyListener implements Selection.Listener, LibraryListener {
        @Override
        public void selectionChanged(Selection.Event e) {
            fireEvent(ProjectEvent.ACTION_SELECTION, e.getSource());
        }

        @Override
        public void libraryChanged(LibraryEvent event) {
            int action = event.getAction();
            if (action == LibraryEvent.REMOVE_LIBRARY) {
                Library unloaded = (Library) event.getData();
                if (tool != null && unloaded.containsFromSource(tool)) {
                    setTool(null);
                }
            } else if (action == LibraryEvent.REMOVE_TOOL) {
                Object data = event.getData();
                if (data instanceof AddTool) {
                    Object factory = ((AddTool) data).getFactory();
                    if (factory instanceof SubcircuitFactory) {
                        SubcircuitFactory fact = (SubcircuitFactory) factory;
                        if (fact.getSubcircuit() == getCurrentCircuit()) {
                            setCurrentCircuit(file.getMainCircuit());
                        }
                    }
                }
            }
        }
    }

    private Simulator simulator = new Simulator();
    private LogisimFile file;
    private CircuitState circuitState;
    private HashMap<Circuit,CircuitState> stateMap
        = new HashMap<Circuit,CircuitState>();
    private Frame frame = null;
    private OptionsFrame optionsFrame = null;
    private LogFrame logFrame = null;
    private Tool tool = null;
    private LinkedList<ActionData> undoLog = new LinkedList<ActionData>();
    private int undoMods = 0;
	private LinkedList<ActionData> redoLog = new LinkedList<ActionData>();
	private int redoMods = 0;
    private EventSourceWeakSupport<ProjectListener> projectListeners
        = new EventSourceWeakSupport<ProjectListener>();
    private EventSourceWeakSupport<LibraryListener> fileListeners
        = new EventSourceWeakSupport<LibraryListener>();
    private EventSourceWeakSupport<CircuitListener> circuitListeners
        = new EventSourceWeakSupport<CircuitListener>();
    private Dependencies depends;
    private MyListener myListener = new MyListener();
    private boolean startupScreen = false;

    public Project(LogisimFile file) {
        addLibraryListener(myListener);
        setLogisimFile(file);
    }

    public void setFrame(Frame value) {
        if (frame == value) {
            return;
        }

        Frame oldValue = frame;
        frame = value;
        Projects.windowCreated(this, oldValue, value);
        value.getCanvas().getSelection().addListener(myListener);
    }

    //
    // access methods
    //
    public LogisimFile getLogisimFile() {
        return file;
    }

    public Simulator getSimulator() {
        return simulator;
    }

    public Options getOptions() {
        return file.getOptions();
    }

    public Dependencies getDependencies() {
        return depends;
    }

    public Frame getFrame() {
        return frame;
    }

    public OptionsFrame getOptionsFrame(boolean create) {
        if (optionsFrame == null || optionsFrame.getLogisimFile() != file) {
            if (create) {
                optionsFrame = new OptionsFrame(this);
            }

            else {
                optionsFrame = null;
            }

        }
        return optionsFrame;
    }

    public LogFrame getLogFrame(boolean create) {
        if (logFrame == null) {
            if (create) {
                logFrame = new LogFrame(this);
            }

        }
        return logFrame;
    }

    public Circuit getCurrentCircuit() {
        return circuitState == null ? null : circuitState.getCircuit();
    }

    public CircuitState getCircuitState() {
        return circuitState;
    }

    public CircuitState getCircuitState(Circuit circuit) {
        if (circuitState != null && circuitState.getCircuit() == circuit) {
            return circuitState;
        } else {
            CircuitState ret = stateMap.get(circuit);
            if (ret == null) {
                ret = new CircuitState(this, circuit);
                stateMap.put(circuit, ret);
            }
            return ret;
        }
    }

	/** Decide whether or not you can redo
	 * @return if we can redo
	 */
	public boolean getCanRedo()
	{
		// If there's a redo option found...
		if( redoLog.size() > 0 )
			// We can redo
			return true;
		else
			// Otherwise we can't.
			return false;
	}

    public Action getLastAction() {
        if (undoLog.size() == 0) {
            return null;
        } else {
            return undoLog.getLast().action;
        }
    }

	/** Returns the action of the last entry in the redo log
	 * @return last action in redo log
	 */
	public Action getLastRedoAction()
	{
		if( redoLog.size() == 0 )
			return null;
		else
			return redoLog.getLast().action;
	}

    public Tool getTool() {
        return tool;
    }

    public Selection getSelection() {
        if (frame == null) {
            return null;
        }

        Canvas canvas = frame.getCanvas();
        if (canvas == null) {
            return null;
        }

        return canvas.getSelection();
    }

    public boolean isFileDirty() {
        return undoMods != 0;
    }

    public JFileChooser createChooser() {
        if (file == null) {
            return JFileChoosers.create();
        }

        Loader loader = file.getLoader();
        return loader == null ? JFileChoosers.create() : loader.createChooser();
    }

    //
    // Listener methods
    //
    public void addProjectListener(ProjectListener what) {
        projectListeners.add(what);
    }

    public void removeProjectListener(ProjectListener what) {
        projectListeners.remove(what);
    }

    public void addLibraryListener(LibraryListener value) {
        fileListeners.add(value);
        if (file != null) {
            file.addLibraryListener(value);
        }

    }

    public void removeLibraryListener(LibraryListener value) {
        fileListeners.remove(value);
        if (file != null) {
            file.removeLibraryListener(value);
        }

    }

    public void addCircuitListener(CircuitListener value) {
        circuitListeners.add(value);
        Circuit current = getCurrentCircuit();
        if (current != null) {
            current.addCircuitListener(value);
        }

    }

    public void removeCircuitListener(CircuitListener value) {
        circuitListeners.remove(value);
        Circuit current = getCurrentCircuit();
        if (current != null) {
            current.removeCircuitListener(value);
        }

    }

    private void fireEvent(int action, Object old, Object data) {
        fireEvent(new ProjectEvent(action, this, old, data));
    }

    private void fireEvent(int action, Object data) {
        fireEvent(new ProjectEvent(action, this, data));
    }

    private void fireEvent(ProjectEvent event) {
        for (ProjectListener l : projectListeners) {
            l.projectChanged(event);
        }
    }

    // We track whether this project is the empty project opened
    // at startup by default, because we want to close it
    // immediately as another project is opened, if there
    // haven't been any changes to it.
    public boolean isStartupScreen() {
        return startupScreen;
    }

    public boolean confirmClose(String title) {
        return frame.confirmClose(title);
    }

    //
    // actions
    //
    public void setStartupScreen(boolean value) {
        startupScreen = value;
    }

    public void setLogisimFile(LogisimFile value) {
        LogisimFile old = this.file;
        if (old != null) {
            for (LibraryListener l : fileListeners) {
                old.removeLibraryListener(l);
            }
        }
        file = value;
        stateMap.clear();
        depends = new Dependencies(file);
        undoLog.clear();
        undoMods = 0;
        fireEvent(ProjectEvent.ACTION_SET_FILE, old, file);
        setCurrentCircuit(file.getMainCircuit());
        if (file != null) {
            for (LibraryListener l : fileListeners) {
                file.addLibraryListener(l);
            }
        }
        // toggle it so that everybody hears the file is fresh
        file.setDirty(true);
        file.setDirty(false);
    }

    public void setCircuitState(CircuitState value) {
        if (value == null || circuitState == value) {
            return;
        }


        CircuitState old = circuitState;
        Circuit oldCircuit = old == null ? null : old.getCircuit();
        Circuit newCircuit = value.getCircuit();
        boolean circuitChanged = old == null || oldCircuit != newCircuit;
        if (circuitChanged) {
            Canvas canvas = frame == null ? null : frame.getCanvas();
            if (canvas != null) {
                if (tool != null) {
                    tool.deselect(canvas);
                }

                Selection selection = canvas.getSelection();
                if (selection != null) {
                    Action act = SelectionActions.dropAll(selection);
                    if (act != null) {
                        doAction(act);
                    }
                }
                if (tool != null) {
                    tool.select(canvas);
                }

            }
            if (oldCircuit != null) {
                for (CircuitListener l : circuitListeners) {
                    oldCircuit.removeCircuitListener(l);
                }
            }
        }
        circuitState = value;
        stateMap.put(circuitState.getCircuit(), circuitState);
        simulator.setCircuitState(circuitState);
        if (circuitChanged) {
            fireEvent(ProjectEvent.ACTION_SET_CURRENT, oldCircuit, newCircuit);
            if (newCircuit != null) {
                for (CircuitListener l : circuitListeners) {
                    newCircuit.addCircuitListener(l);
                }
            }
        }
        fireEvent(ProjectEvent.ACTION_SET_STATE, old, circuitState);
    }

    public void setCurrentCircuit(Circuit circuit) {
        CircuitState circState = stateMap.get(circuit);
        if (circState == null) {
            circState = new CircuitState(this, circuit);
        }

        setCircuitState(circState);
    }

    public void setTool(Tool value) {
        if (tool == value) {
            return;
        }

        Tool old = tool;
        Canvas canvas = frame.getCanvas();
        if (old != null) {
            old.deselect(canvas);
        }

        Selection selection = canvas.getSelection();
        if (selection != null && !selection.isEmpty()) {
            if (value == null || !getOptions().getMouseMappings().containsSelectTool()) {
                Action act = SelectionActions.anchorAll(selection);
                if (act != null) {
                    doAction(act);
                }
            }
        }
        startupScreen = false;
        tool = value;
        if (tool != null) {
            tool.select(frame.getCanvas());
        }

        fireEvent(ProjectEvent.ACTION_SET_TOOL, old, tool);
    }

    public void doAction(Action act) {
        if (act == null) {
            return;
        }

        Action toAdd = act;
        startupScreen = false;
		
		if (!undoLog.isEmpty() && act.shouldAppendTo(getLastAction()))
		{
            ActionData firstData = undoLog.removeLast();
            Action first = firstData.action;
			
			if (first.isModification())
			{
				--undoMods;
			}
			
            toAdd = first.append(act);
            if (toAdd != null) {
                undoLog.add(new ActionData(circuitState, toAdd));
                if (toAdd.isModification()) {
                    ++undoMods;
                }

            }
            fireEvent(new ProjectEvent(ProjectEvent.ACTION_START, this, act));
            act.doIt(this);
            file.setDirty(isFileDirty());
            fireEvent(new ProjectEvent(ProjectEvent.ACTION_COMPLETE, this, act));
            fireEvent(new ProjectEvent(ProjectEvent.ACTION_MERGE, this, first, toAdd));
            return;
        }
        undoLog.add(new ActionData(circuitState, toAdd));
        fireEvent(new ProjectEvent(ProjectEvent.ACTION_START, this, act));
        act.doIt(this);
        while (undoLog.size() > MAX_UNDO_SIZE) {
            undoLog.removeFirst();
        }
        if (toAdd.isModification()) {
            ++undoMods;
        }

        file.setDirty(isFileDirty());
        fireEvent(new ProjectEvent(ProjectEvent.ACTION_COMPLETE, this, act));
    }

	public void undoAction()
	{
		if( undoLog != null && undoLog.size() > 0 )
		{
			redoLog.addLast( undoLog.getLast() );
			++redoMods;
            ActionData data = undoLog.removeLast();
            setCircuitState(data.circuitState);
            Action action = data.action;
            if (action.isModification()) {
                --undoMods;
            }

            fireEvent(new ProjectEvent(ProjectEvent.UNDO_START, this, action));
            action.undo(this);
            file.setDirty(isFileDirty());
            fireEvent(new ProjectEvent(ProjectEvent.UNDO_COMPLETE, this, action));
		}
	}

	/** Redo actions that were previously undone
	 */
	public void redoAction()
	{
		// If there ARE things to undo...
		if( redoLog != null && redoLog.size() > 0 )
		{
			// Add the last element of the undo log to the redo log
			undoLog.addLast( redoLog.getLast() );
			
			// Remove the last item in the redo log, but keep the data
			ActionData data = redoLog.removeLast();

			// Restore the circuit state to the redo's state
			setCircuitState( data.circuitState );

			// Get the actions required to make that state change happen
			Action action = data.action;

			// Is this action a modification?
			if( action.isModification() )
				// Yes? Then get rid of the mod
				--redoMods;
			
			// Call the event
			fireEvent( new ProjectEvent( ProjectEvent.REDO_START, this, action ) );

			// Redo the action
			action.doIt( this );

			// Complete the redo
			fireEvent( new ProjectEvent( ProjectEvent.REDO_COMPLETE, this, action ) );
        }
    }

    public void setFileAsClean() {
        undoMods = 0;
        file.setDirty(isFileDirty());
    }

    public void repaintCanvas() {
        // for actions that ought not be logged (i.e., those that
        // change nothing, except perhaps the current values within
        // the circuit)
        fireEvent(new ProjectEvent(ProjectEvent.REPAINT_REQUEST, this, null));
    }
}
